FE-629: Compile Petrinaut user code through an HIR to buffer-native programs#8981
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
PR SummaryHigh Risk Overview Runtime contract: lambdas/kernels/metrics now take f64/u64/u8 views and offsets instead of decoded Supporting changes: actual-mode timeline frames pack count-only coloured markings into token buffers so HIR metrics work there; numeric markings are normalized with Reviewed by Cursor Bugbot for commit 8ac159e. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Pull request overview
This PR replaces Petrinaut's Babel + eval execution of user code (dynamics, firing rates, transition kernels, metrics) with a new HIR (high-level intermediate representation) pipeline. User code is lowered from a TypeScript subset to a typed, spanned, JSON-serializable expression tree, typechecked against the token schema, analyzed (distribution DAGs, dependency sets, determinism), and emitted as buffer-native programs that read/write packed frame buffers directly with attribute offsets/strides baked in as constants. This removes @babel/standalone and per-call token decode/encode from the simulation hot loops (worker bundles shrink ~3 MB → ~40 kB), surfaces out-of-subset code as blocking LSP error diagnostics, and threads compiled artifacts through the simulation/Monte-Carlo start flows. It fits into the petrinaut-core/petrinaut packages as deliberately private API and lays groundwork for a future DSL frontend and WASM/GPU backends.
Changes:
- New HIR pipeline in
petrinaut-core/src/hir/(lowering, typecheck, analyses, lints, buffer emitters) plus engine execution of buffer programs and deletion of Babel/compile-user-code/compile-metric/record adapters. - Artifact compilation moved into the LSP worker (
sdcpn/compileHirArtifacts) and threaded through simulation/experiment configs; metric drawers and the timeline now rely on LSP diagnostics + async HIR artifacts instead of synchronouscompileMetric. - LSP/UX: HIR lints (codes
99001+,source: "hir"), error-only Play gating with an amber warnings-only indicator, and updated user docs.
Reviewed changes
Copilot reviewed 95 out of 96 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
yarn.lock |
Drops @babel/standalone/@types/babel__standalone; adds optional typescript peer dependency for petrinaut-core |
.../metrics/view-metric-drawer.tsx |
Removes synchronous compileMetric compile-check; validation now sourced from metric LSP diagnostics |
.../metrics/create-metric-drawer.tsx |
Same removal of live compileMetric preview compile in the create flow |
.../metrics/metric-form.tsx |
Updates comment to reflect HIR-based compilability rather than compileMetric |
.../experiments/create-experiment-drawer.tsx |
buildMetricSpecs returns ExperimentMetricSpecInput; expression metrics validated for non-empty code, compiled via HIR at experiment start |
.../simulation-timeline/use-streaming-data.ts |
New async useTimelineMetric hook compiling the selected metric via requestHirArtifacts + createHirMetricEvaluator |
.../BottomBar/diagnostics-indicator.tsx |
Adds amber warning status for warnings/hints-only state (Play still allowed) |
petrinaut-core/src/hir.ts (+ hir-runtime) |
New public entry exposing compileHirArtifacts and HIR types |
.../simulation/engine/buffer-transition.ts |
New buffer-ABI transition execution (kernel sinks, UUID lanes, staging) |
.../simulation/engine/build-simulation.ts |
Instantiates buffer programs, validates stale/missing artifacts, allocates reusable scratch |
.../simulation/frames/hir-metric.ts |
New HIR metric evaluator with cached place indices + per-run string-pool rebind |
.../simulation/frames/frame-reader.ts, simulation/api.ts |
getPlaceTokens gains optional color? param (currently ignored) |
.../lsp/lib/check-hir.ts |
HIR diagnostic-code registry and per-item HIR linting (subsetSeverity: "error") |
.../simulation-timeline/series-config/metric.ts |
Timeline metric config switches from places/buildMetricState to an injected evaluator |
react/experiments/context |
Adds ExperimentMetricSpecInput type for pre-artifact metric specs |
docs: petri-net-extensions.md, simulation.md |
Document supported subset (incl. metrics), error/warning split, and error-only Play gating |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #8981 +/- ##
==========================================
- Coverage 59.68% 59.68% -0.01%
==========================================
Files 1371 1371
Lines 135555 135555
Branches 6066 6066
==========================================
- Hits 80912 80911 -1
- Misses 53711 53712 +1
Partials 932 932 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will degrade performance by 15.38%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | bit_matrix/dense/iter_row[64] |
140.8 ns | 170 ns | -17.16% |
| ❌ | bit_matrix/dense/iter_row[200] |
185.8 ns | 215 ns | -13.57% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing cf/fe-629-add-an-intermediate-representation-for-petrinaut (8ac159e) with main (82dc297)1
Footnotes
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | Flame Graph |
representative_read_entity
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |
🌟 What is the purpose of this PR?
Replace Petrinaut’s Babel-based user-code path with a typed HIR pipeline. Dynamics, transition rates, kernels, and expression metrics are lowered and checked in the language worker, then emitted as buffer-native programs for simulation workers.
This enables static diagnostics and analysis while removing per-call token-record decoding from simulation hot paths. Raw user text is never evaluated directly at runtime.
🔍 What does this change?
petrinaut-core.f64/u64/u8frame views with baked attribute offsets and strides.99001+); unsupported or non-compilable code is a blocking error, while warnings/hints leave Play available.@babel/standalone, the legacy user-code and metric compilers, record decode/encode adapters, the exploratory DSL sketch, the HIR Playground Storybook story, and its unmaintained benchmark harness.Scenarios and visualizers are unchanged: scenarios retain their once-per-run sandboxed path, and visualizers remain UI JSX.
User TypeScript outside the supported analyzable subset now produces an editor error instead of executing. The shipped examples compile through the HIR pipeline.
🔗 Related links
Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
🛡 What tests cover this?
❓ How to test this?
yarn workspace @hashintel/petrinaut-core test:unit run.